package com.sloan.community.service;

import com.sloan.community.common.Constant;
import com.sloan.community.common.command.ArticleCommand;
import com.sloan.community.common.command.EditArticleCommand;
import com.sloan.community.common.query.ArticleSearchQuery;
import com.sloan.community.common.vo.*;
import com.sloan.community.exception.CommunityException;
import com.sloan.community.po.*;
import com.sloan.community.repository.*;
import com.sloan.community.util.DateUtils;
import com.sloan.community.util.JsoupUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.GroupedFlux;
import reactor.core.publisher.Mono;

import java.nio.ByteBuffer;
import java.time.LocalDateTime;
import java.util.*;

@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class ArticleService {

    private final ArticleRepository articleRepository;
    private final ArticleContentRepository articleContentRepository;
    private final DatabaseClient databaseClient;
    private final TagRepository tagRepository;
    private final ArticleTagRepository articleTagRepository;
    private final TagParentRepository tagParentRepository;

    @Transactional(rollbackFor = Exception.class)
    public Mono<ArticleVO> add(ArticleCommand articleReq, User user) {
        Flux<Tag> tagFlux = Flux.fromIterable(articleReq.getTagIds())
                .flatMap(tagId -> tagRepository.findById(tagId)
                        .switchIfEmpty(Constant.tagNotFoundException(tagId)));

        Article article = new Article();
        BeanUtils.copyProperties(articleReq, article);
        article.setUserId(user.getId());
        article.setView(0L);
        article.setLikeCount(0L);
        article.setShareCount(0L);

        return Mono.when(tagFlux)
                .then(articleRepository.save(article)
                        .flatMap(dbArticle -> {
                            ArticleContent content = new ArticleContent();
                            content.setArticleId(dbArticle.getId());
                            content.setContent(articleReq.getContent());
                            content.setExtractText(JsoupUtils.extractContent(articleReq.getContent()));
                            return articleContentRepository.save(content);
                        }).then(Flux.fromIterable(articleReq.getTagIds())
                                .flatMap(tagId -> {
                                    ArticleTag articleTag = new ArticleTag();
                                    articleTag.setArticleId(article.getId());
                                    articleTag.setTagId(tagId);
                                    return articleTagRepository.save(articleTag);
                                }).then(Mono.fromSupplier(() -> {
                                    ArticleVO articleVO = new ArticleVO();
                                    BeanUtils.copyProperties(article, articleVO);
                                    articleVO.setTagIds(articleReq.getTagIds());
                                    return articleVO;
                                }))));
    }

    public Mono<ArticleVO> detail(Long id, Long userId) {
        if (Objects.isNull(id)) {
            return this.getTagParentFlux(Flux.just(0L));
        }

        Mono<Article> articleMono = articleRepository.findById(id)
                .switchIfEmpty(Constant.articleNotFoundException(id))
                .handle((dbArticle, sink) -> {
                    if (!dbArticle.getUserId().equals(userId)) {
                        sink.error(CommunityException.of("非法操作"));
                        return;
                    }
                    sink.next(dbArticle);
                });

        Mono<ArticleContent> contentMono = articleContentRepository.findByArticleId(id);

        Flux<Long> tagIdFlux = articleTagRepository.findByArticleId(id)
                .map(ArticleTag::getTagId);

        Flux<TagVO> articleTagFlux = tagIdFlux.flatMap(tagId -> {
            return tagRepository.findById(tagId)
                    .map(tag -> {
                        TagVO tagVO = new TagVO();
                        BeanUtils.copyProperties(tag, tagVO);
                        return tagVO;
                    });
        });


        return Mono.zip(articleMono, contentMono, tagIdFlux.collectList(),
                        articleTagFlux.collectList(), getTagParentFlux(tagIdFlux))
                .map(tuple -> {
                    ArticleVO t5 = tuple.getT5();
                    BeanUtils.copyProperties(tuple.getT1(), t5);
                    t5.setContent(tuple.getT2().getContent());
                    t5.setTagIds(tuple.getT3());
                    t5.setTags(tuple.getT4());
                    return t5;
                });
    }

    public Mono<ArticleVO> getInitTags(Flux<Long> tagIdFlux) {
        Flux<TagParentVO> tagParentFlux = tagParentRepository.findAll()
                .sort(Comparator.comparing(TagParent::getId))
                .map(tagParent -> {
                    TagParentVO tagParentVO = new TagParentVO();
                    BeanUtils.copyProperties(tagParent, tagParentVO);
                    return tagParentVO;
                });

        Flux<List<Tag>> tagFluxMap = tagRepository.findAll()
                .groupBy(Tag::getParentId)
                .flatMap(group -> group.collectList()
                        .map(item -> new AbstractMap.SimpleEntry<>(group.key(), item)))
                .sort(Map.Entry.comparingByKey())
                .map(AbstractMap.SimpleEntry::getValue);

        return Mono.zip(tagParentFlux.collectList(),
                        tagFluxMap.collectList(), tagIdFlux.collectList())
                .map(tuple -> {
                    Set<Long> tagIds = new HashSet<>(tuple.getT3());
                    List<List<TagVO>> tags = tuple.getT2().stream()
                            .map(list -> list.stream()
                                    .map(tag -> {
                                        TagVO tagVO = new TagVO();
                                        BeanUtils.copyProperties(tag, tagVO);
                                        tagVO.setSelected(tagIds.contains(tag.getId()));
                                        return tagVO;
                                    }).toList()).toList();

                    ArticleVO articleVO = new ArticleVO();
                    articleVO.setTagParents(tuple.getT1());
                    articleVO.setAllTags(tags);
                    return articleVO;
                });
    }


    private Mono<ArticleVO> getTagParentFlux(Flux<Long> tagIdFlux) {
        // 基于value进行分组
        return tagRepository.findAll()
                .groupBy(Tag::getParentId)
                .flatMap(tags -> {
                    Long tagParentId = tags.key();
                    return Mono.zip(tagParentRepository.findById(tagParentId), tags.collectList(), tagIdFlux.collectList())
                            .map(tuple3 -> {
                                Set<Long> tagIds = new HashSet<>(tuple3.getT3());
                                TagParentVO tagParent = new TagParentVO();
                                tagParent.setId(tuple3.getT1().getId());
                                tagParent.setName(tuple3.getT1().getName());
                                tagParent.setTags(tuple3.getT2().stream()
                                        .map(tag -> {
                                            TagVO tagVO = new TagVO();
                                            BeanUtils.copyProperties(tag, tagVO);
                                            tagVO.setSelected(tagIds.contains(tag.getId()));
                                            return tagVO;
                                        }).toList());
                                return tagParent;
                            });
                })
                .collectList()
                .map(item -> {
                    ArticleVO articleVO = new ArticleVO();
                    articleVO.setTagParents(item);
                    articleVO.setCommentable(Boolean.TRUE);
                    articleVO.setRecommend(Boolean.TRUE);
                    articleVO.setAppreciation(Boolean.FALSE);
                    articleVO.setShareable(Boolean.TRUE);
                    return articleVO;
                });
    }

    /**
     * 有BUG
     *
     * @param tagIdFlux
     * @return
     */
    @Deprecated
    private Mono<ArticleVO> getTagList(Flux<Long> tagIdFlux) {
        Flux<TagParentVO> tagParentFlux = tagParentRepository.findAll()
                .sort(Comparator.comparing(TagParent::getId, Comparator.naturalOrder()))
                .map(tagParent -> {
                    TagParentVO parentVO = new TagParentVO();
                    BeanUtils.copyProperties(tagParent, parentVO);
                    return parentVO;
                });

        // Flux<List<Tag>>
        Flux<GroupedFlux<Long, Tag>> groupedFlux = tagRepository.findAll()
                .groupBy(Tag::getParentId)
                .sort(Comparator.comparing(GroupedFlux::key, Comparator.naturalOrder()));

        Mono<List<GroupedFlux<Long, Tag>>> p1 = groupedFlux.collectList();
        Mono<List<Long>> p2 = tagIdFlux.collectList();
        return Mono.zip(p1, p2, tagParentFlux.collectList())
                .map(tuple3 -> {
                    Set<Long> tagIds = new HashSet<>(tuple3.getT2());
                    List<GroupedFlux<Long, Tag>> t1 = tuple3.getT1();
                    List<List<TagVO>> list = t1.stream()
                            .map(tags -> {
                                Mono<List<TagVO>> map = tags.collectList()
                                        .map(tagList -> {
                                            return tagList.stream().map(tag -> {
                                                TagVO tagVO = new TagVO();
                                                BeanUtils.copyProperties(tag, tagVO);
                                                tagVO.setSelected(tagIds.contains(tag.getId()));
                                                return tagVO;
                                            }).toList();
                                        });
                                return map.block();
                            }).toList();
                    ArticleVO articleVO = new ArticleVO();
                    articleVO.setTagParents(tuple3.getT3());
                    articleVO.setAllTags(list);
                    return articleVO;
                });
    }

    public Mono<PageVo<SearchArticleVO>> searchCondition(ArticleSearchQuery param, Long userId) {
        this.transferKeywordToId(param);
        Integer offset = (param.getCurrent() - 1) * param.getSize();
        String sqlQuery = this.createArticleSqlQuery(param, userId, offset);
        String sqlCount = this.createArticleSqlCount(param, userId);
        Mono<Long> count = databaseClient.sql(sqlCount)
                .fetch()
                .one()
                .map(item -> Long.parseLong(item.get("TOTAL").toString()));

        Flux<SearchArticleVO> articleList = databaseClient.sql(sqlQuery)
                .fetch()
                .all()
                .map(item -> {
                    SearchArticleVO articleVO = new SearchArticleVO();
                    articleVO.setId(Long.parseLong(item.get("id").toString()));
                    articleVO.setTitle(item.get("title").toString());
                    if (Objects.nonNull(item.get("first_picture"))) {
                        articleVO.setFirstPicture(item.get("first_picture").toString());
                    }
                    articleVO.setView(Long.parseLong(item.get("view").toString()));
                    articleVO.setLikeCount(Long.parseLong(item.get("like_count").toString()));
                    articleVO.setShareCount(Long.parseLong(item.get("share_count").toString()));
                    articleVO.setShareable(((ByteBuffer) item.get("shareable")).get(0) == 1);
                    articleVO.setCommentable(((ByteBuffer) item.get("commentable")).get(0) == 1);
                    articleVO.setRecommend(((ByteBuffer) item.get("recommend")).get(0) == 1);
                    articleVO.setAppreciation(((ByteBuffer) item.get("appreciation")).get(0) == 1);
                    if (Objects.nonNull(item.get("tags"))) {  // TODO 分号优化
                        articleVO.setTags(Arrays.asList(item.get("tags").toString().split(";")));
                    }
                    if (Objects.nonNull(item.get("created_time"))) {
                        articleVO.setCreatedTime(DateUtils.transfer(item.get("created_time").toString()));
                    }
                    if (Objects.nonNull(item.get("updated_time"))) {
                        articleVO.setUpdatedTime(DateUtils.transfer(item.get("updated_time").toString()));
                    }
                    return articleVO;
                });

        return Mono.zip(count, articleList.collectList())
                .map(item -> new PageVo<SearchArticleVO>(item.getT1(), item.getT2(),
                        param.getCurrent(), param.getSize(), 5, Boolean.FALSE));
    }

    private void transferKeywordToId(ArticleSearchQuery param) {
        try {
            param.setId(Long.parseLong(param.getKeyword()));
        } catch (NumberFormatException e) {
            log.info("转换ID错误:{}", param.getKeyword());
        }
    }

    private String createArticleSqlQuery(ArticleSearchQuery param, Long userId, Integer offset) {
        /**
         * SELECT
         *   ta.*,
         *   GROUP_CONCAT(tt.name,';') AS tags
         * FROM
         *   t_article ta
         *   LEFT JOIN t_article_tag tat ON ta.id = tat.article_id
         *   LEFT JOIN t_tag tt ON tat.tag_id = tt.id
         * WHERE
         *   ta.user_id = 16221011180261376
         *   AND (ta.title LIKE '%搞笑%' OR tt.name LIKE '%搞笑%' OR ta.id = 16232111182188544 )
         *   GROUP BY ta.id
         * ORDER BY
         *   ta.CREATED_TIME DESC
         *   LIMIT 0,
         *   10;
         *
         */
        StringBuilder sb = new StringBuilder();
        String baseQuerySql = """
                SELECT
                    ta.*,
                    GROUP_CONCAT(tt.name,';') AS tags
                FROM
                    t_article ta
                    LEFT JOIN t_article_tag tat ON ta.id = tat.article_id
                    LEFT JOIN t_tag tt ON tat.tag_id = tt.id
                WHERE
                    ta.user_id = 
                """;
        sb.append(baseQuerySql)
                .append(userId);
        if (StringUtils.hasText(param.getKeyword())) {
            sb.append(" AND (ta.title LIKE '%")
                    .append(param.getKeyword())
                    .append("%' OR tt.name LIKE '%")
                    .append(param.getKeyword())
                    .append("%' ");
            if (Objects.nonNull(param.getId())) {
                sb.append(" OR ta.id = ")
                        .append(param.getId());
            }
            sb.append(")");
        }

        sb.append(" GROUP BY ta.id ORDER BY CREATED_TIME DESC ");
        sb.append(" LIMIT ")
                .append(offset)
                .append(",")
                .append(param.getSize());
        return sb.toString();
    }

    private String createArticleSqlCount(ArticleSearchQuery param, Long userId) {
        /**
         * SELECT
         *   COUNT( * ) AS total
         * FROM
         *   (
         *     SELECT
         *       ta.*
         *     FROM
         *       t_article ta
         *       LEFT JOIN t_article_tag tat ON ta.id = tat.article_id
         *       LEFT JOIN t_tag tt ON tat.tag_id = tt.id
         *     WHERE
         *       ta.user_id = 16221011180261376
         *       AND ( ta.title LIKE '%搞笑%' OR tt.NAME LIKE '%搞笑%' OR ta.id = 16232111182188544 )
         *     GROUP BY
         *       ta.id
         *   ) t
         */
        StringBuilder sb = new StringBuilder();
        String baseQuerySql = """
                SELECT
                    COUNT(*) AS total
                FROM (
                    SELECT
                        ta.*,
                        GROUP_CONCAT(tt.name,';') AS tags
                    FROM
                        t_article ta
                        LEFT JOIN t_article_tag tat ON ta.id = tat.article_id
                        LEFT JOIN t_tag tt ON tat.tag_id = tt.id
                    WHERE
                        ta.user_id = 
                """;
        sb.append(baseQuerySql)
                .append(userId);
        if (StringUtils.hasText(param.getKeyword())) {
            sb.append(" AND (ta.title LIKE '%")
                    .append(param.getKeyword())
                    .append("%' OR tt.name LIKE '%")
                    .append(param.getKeyword())
                    .append("%' ");
            if (Objects.nonNull(param.getId())) {
                sb.append(" OR ta.id = ")
                        .append(param.getId());
            }
            sb.append(")");
        }

        sb.append(" GROUP BY ta.id ) t ");
        return sb.toString();
    }

    public Mono<Void> commentSwitch(Long id, Long userId) {
        return articleRepository.findById(id)
                .handle((item, sink) -> {
                    if (!item.getUserId().equals(userId)) {
                        sink.error(CommunityException.of("非法操作"));
                        return;
                    }
                    sink.next(item);
                })
                .then(articleRepository.updateCommentableById(id, userId, LocalDateTime.now()));
    }

    public Mono<Void> appreciationSwitch(Long id, Long userId) {
        return articleRepository.findById(id)
                .handle((item, sink) -> {
                    if (!item.getUserId().equals(userId)) {
                        sink.error(CommunityException.of("非法操作"));
                        return;
                    }
                    sink.next(item);
                })
                .then(articleRepository.updateAppreciationById(id, userId, LocalDateTime.now()));
    }

    public Mono<Void> recommendSwitch(Long id, Long userId) {
        return articleRepository.findById(id)
                .handle((item, sink) -> {
                    if (!item.getUserId().equals(userId)) {
                        sink.error(CommunityException.of("非法操作"));
                        return;
                    }
                    sink.next(item);
                })
                .then(articleRepository.updateRecommendById(id, userId, LocalDateTime.now()));
    }

    @Transactional(rollbackFor = Exception.class)
    public Mono<Void> delete(Long id, Long userId) {
        Mono<Void> article = articleRepository.findById(id)
                .switchIfEmpty(Constant.articleNotFoundException(id))
                .flatMap(item -> {
                    if (!item.getUserId().equals(userId)) {
                        return Mono.error(CommunityException.of("非法操作"));
                    }
                    return articleRepository.delete(item);
                });

        Mono<Void> articleTagMono = articleTagRepository.deleteByArticleId(id);
        Mono<Void> contentMono = articleContentRepository.deleteByArticleId(id);
        return article.then(contentMono).then(articleTagMono);
    }

    @Transactional(rollbackFor = Exception.class)
    public Mono<ArticleVO> update(Long id, ArticleCommand param, Long userId) {
        Mono<Article> articleMono = articleRepository.findById(id)
                .switchIfEmpty(Constant.articleNotFoundException(id))
                .handle((item, sink) -> {
                    if (!item.getUserId().equals(userId)) {
                        sink.error(CommunityException.of("非法操作"));
                        return;
                    }
                    sink.next(item);
                });

        EditArticleCommand editArticleCommand = new EditArticleCommand();
        BeanUtils.copyProperties(param, editArticleCommand);
        editArticleCommand.setId(id);
        editArticleCommand.setUpdatedBy(userId);
        editArticleCommand.setUpdatedTime(LocalDateTime.now());
        Mono<Void> voidMono = articleRepository.editArticle(editArticleCommand);

        Mono<ArticleContent> contentMono = articleContentRepository.findByArticleId(id)
                .flatMap(articleContent -> {
                    articleContent.setContent(param.getContent());
                    articleContent.setExtractText(JsoupUtils.extractContent(param.getContent()));
                    return articleContentRepository.save(articleContent);
                });


        Mono<List<ArticleTag>> then = articleTagRepository.findByArticleId(id)
                .flatMap(articleTagRepository::delete)
                .then(Flux.fromIterable(param.getTagIds())
                        .flatMap(tagId -> {
                            ArticleTag articleTag = new ArticleTag();
                            articleTag.setArticleId(id);
                            articleTag.setTagId(tagId);
                            return articleTagRepository.save(articleTag);
                        }).collectList());

        // when使用的时候内部的Mono<V> 一定是不能在包装有Mono,或者Flux类型，不然无法执行
        return Mono.when(articleMono, voidMono, contentMono, then)
                .then(Mono.fromSupplier(() -> {
                    ArticleVO articleVO = new ArticleVO();
                    BeanUtils.copyProperties(param, articleVO);
                    articleVO.setId(id);
                    // 其他字段暂时不管
                    return articleVO;
                }));
    }

    public Mono<Article> validArticle(Long id, Long userId) {
        return articleRepository.findById(id)
                .handle((item, sink) -> {
                    if (!item.getUserId().equals(userId)) {
                        sink.error(CommunityException.of("非法操作"));
                        return;
                    }
                    sink.next(item);
                });
    }

    public Mono<Void> shareSwitch(Long id, Long userId) {
        return articleRepository.findById(id)
                .handle((item, sink) -> {
                    if (!item.getUserId().equals(userId)) {
                        sink.error(CommunityException.of("非法操作"));
                        return;
                    }
                    sink.next(item);
                })
                .then(articleRepository.updateShareableById(id, userId, LocalDateTime.now()));
    }
}
